﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using nRoute.Utilities;
using nRoute.Mapping;
#if (!SILVERLIGHT)
using System.Collections.Concurrent;
#endif

namespace nRoute.Components.Composition
{
    internal sealed class TypeDescriptor
    {

#region Declarations

        private const string RESOLVECONST_MUSTBE_PUBLIC = "ResolveConstructor Attribute is only supported on public constructors for type '{0}'";
        private const string RESOLVECONST_MUSTBE_INSTANCE = "ResolveConstructor Attribute is not supported on static constructors for type '{0}'";
        private const string RESOLVECONST_ONLYONCE = "ResolveConstructor Attribute can only be applied on one constructor for type '{0}'";
        private const string RESOLVECONST_CANNOTBE_ABSTRACT = "ResolveConstructor Attribute cannot be used on an abstract class type '{0}'";
        private const string CANNOT_RESOLVE_CONST = 
            "Cannot resolve a constructor to use for {0}, optionally explicitly specify using ResolveConstructor Attribute";
        private const string RESOLVEFIELD_MUSTBE_PUBLIC =
            "ResolveConstructor Attribute is only supported on public fields, field '{0}' on type '{1}' is not supported";
        private const string RESOLVEFIELD_MUSTBE_INSTANCE =
            "ResolveConstructor Attribute is not supported on static fields, field '{0}' on type '{1}' is not supported";
        private const string RESOLVEFIELD_MUSTNOTBE_CONSTORREADONLY =
            "ResolveConstructor Attribute is not supported on read-only or constant fields, field '{0}' on type '{1}' is not supported";
        private const string RESOLVEPROP_MUSTBE_PUBLIC =
            "ResolveConstructor Attribute is only supported on public properties, property '{0}' on type '{1}' is not supported";
        private const string RESOLVEPROP_MUSTBE_INSTANCE =
            "ResolveConstructor Attribute is not supported on static properties, property '{0}' on type '{1}' is not supported";
        private const string RESOLVEPROP_MUSTBE_WRITABLE =
            "ResolveConstructor Attribute is only supported on properties with public setter, property '{0}' on type '{1}' is not supported";
        private const string RESOLVEPROP_MUSTNOTBE_PARAMETERIZED =
            "ResolveConstructor Attribute is not supported on properties with parameters, property '{0}' on type '{1}' is not supported";
        private const string DEFAULT_CONST_MUSTBE_SPECIFIED = "No default constructor for type '{0}' has been specified.";
        private const string CONSTRUCTOR_MUSTBE_FORTYPE = "Constructor must be for type '{0}'";
        private const string CONSTRUCTOR_PARAMETER_NOTFOUND = "Parameter of type '{0}', not found in constructor for type '{1}'";
        private const string PROP_MUSTBE_FORTYPE = "Property '{0}' must be for type '{1}'";
        private const string FIELD_MUSTBE_FORTYPE = "Field '{0}' must be for type '{1}'";

        private readonly static Type[] EMPTY_PARAMETERS = new Type[] { };

#endregion

#region Variables

        private readonly Object _lock = new Object();
#if (WINRT)
        private readonly TypeInfo _descriptorType;
#else
        private readonly Type _descriptorType;
#endif
        private ConstructorDescriptor _defaultConstructorDescriptor;
        private List<FieldDescriptor> _fieldDescriptors;
        private List<PropertyDescriptor> _propertyDescriptors;

#endregion

#if (WINRT)
        public TypeDescriptor(TypeInfo type)
#else
        public TypeDescriptor(Type type)
#endif
        {
            Guard.ArgumentNotNull(type, "type");
            _descriptorType = type;
        }

#region Properties

#if (WINRT)
        public TypeInfo DescriptorType
#else
        public Type DescriptorType
#endif
        {
            get { return _descriptorType; }
        }

        public ConstructorDescriptor DefaultConstructorDescriptor
        {
            get { return _defaultConstructorDescriptor; }
        }

        public IEnumerable<FieldDescriptor> FieldsDescriptor
        {
            get { return _fieldDescriptors; }
        }

        public IEnumerable<PropertyDescriptor> PropertiesDescriptor
        {
            get { return _propertyDescriptors; }
        }

#endregion

#region Methods

        public void CreateNewProfile(IResourceCatalog resourceCatalog)
        {
            lock (_lock)
            {
                _defaultConstructorDescriptor = ResolveConstructorDescriptor(resourceCatalog);
                _fieldDescriptors = new List<FieldDescriptor>(ResolveFieldsDescriptor());
                _propertyDescriptors = new List<PropertyDescriptor>(ResolvePropertiesDescriptor());
            }
        }

        public void ProfileConstructor(ConstructorInfo constructorInfo)
        {
            Guard.ArgumentNotNull(constructorInfo, "constructorInfo");
            Guard.ArgumentValue(constructorInfo.DeclaringType.GetTypeInfo() != this.DescriptorType, "constructorInfo", CONSTRUCTOR_MUSTBE_FORTYPE, this.DescriptorType.FullName);

            lock (_lock)
            {
                var _constructorDescriptor = ResolveConstructorDescriptor(constructorInfo);
                Interlocked.Exchange<ConstructorDescriptor>(ref _defaultConstructorDescriptor, _constructorDescriptor);
            }
        }

        public void ProfileConstructorParameter(ParameterInfo parameterInfo, IResourceResolver resourceResolver)
        {
            Guard.ArgumentNotNull(parameterInfo, "constructorInfo");
            Guard.ArgumentNotNull(resourceResolver, "resourceResolver");
            Guard.ArgumentValue(_defaultConstructorDescriptor == null, "parameterInfo", DEFAULT_CONST_MUSTBE_SPECIFIED, this.DescriptorType.FullName);

            lock (_lock)
            {
                var _matchingParameterDescriptor = _defaultConstructorDescriptor.ParameterDescriptors.FirstOrDefault((p) => p.Parameter == parameterInfo);
                if (_matchingParameterDescriptor == null)
                {
                    throw new InvalidOperationException(string.Format(CONSTRUCTOR_PARAMETER_NOTFOUND, parameterInfo.ParameterType.FullName, this.DescriptorType.FullName));
                }
                _matchingParameterDescriptor.Resolver = resourceResolver;
            }
        }

        public void ProfileProperty(PropertyInfo propertyInfo, IResourceResolver resourceResolver)
        {
            Guard.ArgumentNotNull(propertyInfo, "constructorInfo");
            Guard.ArgumentNotNull(resourceResolver, "resourceResolver");
            Guard.ArgumentValue(propertyInfo.DeclaringType.GetTypeInfo() != this.DescriptorType, "propertyInfo", PROP_MUSTBE_FORTYPE, propertyInfo.Name, this.DescriptorType.FullName);

            lock (_lock)
            {
                var _matchingPropertyDescriptor = this.PropertiesDescriptor.FirstOrDefault((p) => p.Property == propertyInfo);
                if (_matchingPropertyDescriptor != null)
                {
                    _matchingPropertyDescriptor.Resolver = resourceResolver;
                }
                else
                {
                    _matchingPropertyDescriptor = ResolvePropertyDescriptor(propertyInfo);
                    _matchingPropertyDescriptor.Resolver = resourceResolver;
                    _propertyDescriptors.Add(_matchingPropertyDescriptor);
                }
            }
        }

        public void ProfileField(FieldInfo fieldInfo, IResourceResolver resourceResolver)
        {
            Guard.ArgumentNotNull(fieldInfo, "constructorInfo");
            Guard.ArgumentNotNull(resourceResolver, "resourceResolver");
            Guard.ArgumentValue(fieldInfo.DeclaringType.GetTypeInfo() != this.DescriptorType, "fieldInfo", FIELD_MUSTBE_FORTYPE, fieldInfo.Name, this.DescriptorType.FullName);
            
            lock (_lock)
            {
                var _matchingFieldDescriptor = this.FieldsDescriptor.FirstOrDefault((p) => p.Field == fieldInfo);
                if (_matchingFieldDescriptor != null)
                {
                    _matchingFieldDescriptor.Resolver = resourceResolver;
                }
                else
                {
                    _matchingFieldDescriptor = ResolveFieldDescriptor(fieldInfo);
                    _matchingFieldDescriptor.Resolver = resourceResolver;
                    _fieldDescriptors.Add(_matchingFieldDescriptor);
                }
            }
        }

#endregion

#region Helpers

        private ConstructorDescriptor ResolveConstructorDescriptor(IResourceCatalog resourceCatalog)
        {
            var _consuctorInfo = ResolveConstructorInfo(resourceCatalog);
            return ResolveConstructorDescriptor(_consuctorInfo);
        }

        private ConstructorDescriptor ResolveConstructorDescriptor(ConstructorInfo constructorInfo)
        {
            // get the constructor
            var _constructorDescriptor = new ConstructorDescriptor();
            _constructorDescriptor.Constructor = constructorInfo;

            // resolve parameters
            var _parameterInfos = _constructorDescriptor.Constructor.GetParameters();
            if (_parameterInfos.Length == 0)
            {
#if (!WINRT)
                _constructorDescriptor.ConstructorDelegate = TypeActivator.CreateConstructorDelegate(_constructorDescriptor.Constructor);
#endif
                _constructorDescriptor.ParameterDescriptors = Enumerable.Empty<ParameterDescriptor>();
            }
            else
            {
#if (!WINRT)
                _constructorDescriptor.ConstructorParameterDelegate = TypeActivator.CreateConstructorParametersDelegate(_constructorDescriptor.Constructor);
#endif
                _constructorDescriptor.ParameterDescriptors = ResolveConstructorParametersDescriptor(_parameterInfos);
            }
            return _constructorDescriptor;
        }

        private ConstructorInfo ResolveConstructorInfo(IResourceCatalog resourceCatalog)
        {
#if (SILVERIGHT)
            var _constructorInfos = DescriptorType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
#elif (WINRT)
            var _constructorInfos = DescriptorType.DeclaredConstructors.Where((ci) => ci.IsPublic && !ci.IsStatic);
#else
            var _constructorInfos = DescriptorType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
#endif

            // Option 1. we look for a ResolveConstructor attribute
            var _constructor = (ConstructorInfo)null;
            foreach (var _constructorInfo in _constructorInfos)
            {
                
#if (WINRT)
                if (_constructorInfo.GetCustomAttribute<ResolveConstructorAttribute>(true) != null)
#else
                if (Attribute.IsDefined(_constructorInfo, typeof(ResolveConstructorAttribute)))
#endif
                {

                    // check for non-public constructors, specifically for SL/WINRT
#if (SILVERLIGHT || WINRT)
                    if (!_constructorInfo.IsPublic)
                    {
                        throw new ResolveResourceException(string.Format(RESOLVECONST_MUSTBE_PUBLIC, DescriptorType.FullName), DescriptorType);
                    }
#endif
                    // check non-static
                    if (_constructorInfo.IsStatic)
                    {
                        throw new ResolveResourceException(string.Format(RESOLVECONST_MUSTBE_INSTANCE, DescriptorType.FullName), DescriptorType);
                    }

                    // check if applied more than once
                    if (_constructor != null)
                    {
                        throw new ResolveResourceException(string.Format(RESOLVECONST_ONLYONCE, DescriptorType.FullName), DescriptorType);
                    }
                    
                    // check if abstract / non-usable
                    if (DescriptorType.IsAbstract)
                    {
                        throw new ResolveResourceException(string.Format(RESOLVECONST_CANNOTBE_ABSTRACT, DescriptorType.FullName), DescriptorType);
                    }

                    _constructor = _constructorInfo;
                }
            }

            // Option 2. we look at all parameterized constructors to see if we can server each and every parameter
            // note we do so in a descending order, i.e. we look to fulfil the most parameters we can
            if (_constructor == null)
            {
                foreach (var _constructorInfo in _constructorInfos.OrderByDescending((c) => c.GetParameters().Length))
                {
                    var _parameters = _constructorInfo.GetParameters();
                    if (_parameters.Length > 0)
                    {
                        if (_parameters.All((p) => resourceCatalog.IsResourceable(p.ParameterType) || HasResolveResourceAttribute(p)))
                        {
                            _constructor = _constructorInfo;
                            break;
                        }
                    }
                }
            }

            // Option 3.
            // We try to get a public and parameter-less constructor
            if (_constructor == null)
            {
                foreach (var _constructorInfo in _constructorInfos.OrderByDescending((c) => c.GetParameters().Length))
                {
                    var _parameters = _constructorInfo.GetParameters();
                    if (_parameters.Length == 0)
                    {
                        if (_parameters.All((p) => resourceCatalog.IsResourceable(p.ParameterType)))
                        {
                            _constructor = _constructorInfo;
                            break;
                        }
                    }
                }
            }

            // all else thow an error
            if (_constructor == null)
            {
                throw new ResolveResourceException(string.Format(CANNOT_RESOLVE_CONST, DescriptorType.FullName), DescriptorType);
            }

            // and return
            return _constructor;
        }

        private IEnumerable<ParameterDescriptor> ResolveConstructorParametersDescriptor(IEnumerable<ParameterInfo> parameterInfos)
        {
            Guard.ArgumentNotNull(parameterInfos, "parameterInfos");

            var _parameters = new List<ParameterDescriptor>();
            foreach (var _parameterInfo in parameterInfos)
            {
#if (WINRT)
                if (_parameterInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false) != null)
#else
                if (Attribute.IsDefined(_parameterInfo, typeof(ResolveResourceBaseAttribute), false))
#endif
                {
#if (WINRT)
                    var _attribute = _parameterInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false);
#else
                    var _attribute = Attribute.GetCustomAttribute(_parameterInfo, typeof(ResolveResourceBaseAttribute));
#endif
                    _parameters.Add(new ParameterDescriptor()
                    {
                        Parameter = _parameterInfo,
                        Resolver = (IResourceResolver)_attribute
                    });
                }
                else
                {
                    _parameters.Add(new ParameterDescriptor()
                    {
                        Parameter = _parameterInfo,
                        Resolver = ResourceCatalogTypeResolver.Instance
                    });
                }
            }

            return _parameters;
        }

        private IEnumerable<FieldDescriptor> ResolveFieldsDescriptor()
        {
            var _fields = new List<FieldDescriptor>();
#if (WINRT)
            var _fieldsInfo = _descriptorType.DeclaredFields;
#else
            var _fieldsInfo = _descriptorType.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Instance | 
               BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
#endif

            foreach (var _fieldInfo in _fieldsInfo)
            {
#if (WINRT)
                if (_fieldInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false) != null)
#else
                if (Attribute.IsDefined(_fieldInfo, typeof(ResolveResourceBaseAttribute), false))
#endif
                {
                    var _fieldDescriptor = ResolveFieldDescriptor(_fieldInfo);
#if (WINRT)
                    _fieldDescriptor.Resolver = (IResourceResolver)_fieldInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false);
#else
                    _fieldDescriptor.Resolver = (IResourceResolver)Attribute.GetCustomAttribute(_fieldInfo, typeof(ResolveResourceBaseAttribute));
#endif
                    _fields.Add(_fieldDescriptor);
                }
            }

            return _fields;
        }

        private FieldDescriptor ResolveFieldDescriptor(FieldInfo fieldInfo)
        {
            // 1. check for non public 
#if (SILVERLIGHT || WINRT)
            if (!fieldInfo.IsPublic)
            {
                throw new ResolveResourceException(string.Format(RESOLVEFIELD_MUSTBE_PUBLIC, fieldInfo.Name, DescriptorType), DescriptorType);
            }
#endif

            // 2. check for static felds
            if (fieldInfo.IsStatic)
            {
                throw new ResolveResourceException(string.Format(RESOLVEFIELD_MUSTBE_INSTANCE, fieldInfo.Name, DescriptorType), DescriptorType);
            }

            // 3. check for readonly fields
            if (fieldInfo.IsLiteral || fieldInfo.IsInitOnly)
            {
                throw new ResolveResourceException(string.Format(RESOLVEFIELD_MUSTNOTBE_CONSTORREADONLY, fieldInfo.Name, DescriptorType), DescriptorType);
            }

            // and return
            return new FieldDescriptor()
            {
                Field = fieldInfo
#if (!WINRT)
,
                FieldSetterDelegate = TypeActivator.CreateFieldDelegate(fieldInfo)
#endif
            };
        }

        private IList<PropertyDescriptor> ResolvePropertiesDescriptor()
        {
            var _properties = new List<PropertyDescriptor>();
#if (WINRT)
            var _propertiesInfos = _descriptorType.DeclaredProperties;
#else
            var _propertiesInfos = _descriptorType.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance |
                BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
#endif

            foreach (var _propertyInfo in _propertiesInfos)
            {
#if (WINRT)
                if (_propertyInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false) != null)
#else
                if (Attribute.IsDefined(_propertyInfo, typeof(ResolveResourceBaseAttribute), false))
#endif
                {
                    var _propertyDescriptor = ResolvePropertyDescriptor(_propertyInfo);
#if (WINRT)
                    _propertyDescriptor.Resolver = (IResourceResolver)_propertyInfo.GetCustomAttribute<ResolveResourceBaseAttribute>(false);
#else
                    _propertyDescriptor.Resolver = (IResourceResolver)Attribute.GetCustomAttribute(_propertyInfo, typeof(ResolveResourceBaseAttribute));
#endif
                    _properties.Add(_propertyDescriptor);
                }
            }

            return _properties;
        }

        private PropertyDescriptor ResolvePropertyDescriptor(PropertyInfo propertyInfo)
        {
#if (WINRT)
            var _propertySetter = propertyInfo.SetMethod;
#else
            var _propertySetter = propertyInfo.GetSetMethod(true);
#endif

            // 1. check for non writtable properties
            if (!propertyInfo.CanWrite)
            {
                throw new ResolveResourceException(string.Format(RESOLVEPROP_MUSTBE_WRITABLE, propertyInfo.Name, DescriptorType), DescriptorType);
            }

            // 2. check for non-public properties
#if (SILVERLIGHT || WINRT)
            if (!_propertySetter.IsPublic)
            {
                throw new ResolveResourceException(string.Format(RESOLVEPROP_MUSTBE_PUBLIC, propertyInfo.Name, DescriptorType), DescriptorType);
            }
#endif

            // 3. check for static
            if (_propertySetter.IsStatic)
            {
                throw new ResolveResourceException(string.Format(RESOLVEPROP_MUSTBE_INSTANCE, propertyInfo.Name, DescriptorType), DescriptorType);
            }

            // 4. check for parameterized props
            if (propertyInfo.GetIndexParameters().Length > 0)
            {
                throw new ResolveResourceException(string.Format(RESOLVEPROP_MUSTNOTBE_PARAMETERIZED, propertyInfo.Name, DescriptorType), DescriptorType);
            }

            // and return
            return new PropertyDescriptor()
            {
                Property = propertyInfo
#if (!WINRT)
,
                PropertySetterDelegate = TypeActivator.CreatePropertyDelegate(propertyInfo)
#endif
            };
        }

#endregion

#region Nested Classes

        public sealed class ConstructorDescriptor
        {
            public ConstructorInfo Constructor { get; set; }

            public IEnumerable<ParameterDescriptor> ParameterDescriptors { get; set; }

            public TypeActivator.ConstructorDelegate ConstructorDelegate { get; set; }

            public TypeActivator.ConstructorParamsDelegate ConstructorParameterDelegate { get; set; }
        }

        public sealed class ParameterDescriptor
        {
            public ParameterInfo Parameter { get; set; }

            public IResourceResolver Resolver { get; set; }
        }
        
        public sealed class FieldDescriptor
        {
            public FieldInfo Field { get; set;  }

            public IResourceResolver Resolver { get; set; }

            public TypeActivator.SetterDelegate FieldSetterDelegate { get; set; }
        }

        public sealed class PropertyDescriptor
        {
            public PropertyInfo Property { get; set; }

            public IResourceResolver Resolver { get; set; }

            public TypeActivator.SetterDelegate PropertySetterDelegate { get; set; }
        }

        public sealed class ResourceCatalogTypeResolver
            : IResourceResolver
        {
            private static readonly ResourceCatalogTypeResolver _instance = new ResourceCatalogTypeResolver();

            private ResourceCatalogTypeResolver() { }

            public static IResourceResolver Instance
            {
                get { return _instance; }
            }

#region IResolveTarget Members

            Object IResourceResolver.GetInstance(Type targetType, ITypeBuilder typeBuilder)
            {
                Guard.ArgumentNotNull(targetType, "targetType");
                Guard.ArgumentNotNull(typeBuilder, "typeBuilder");

                return typeBuilder.ResourceCatalog.GetResource(targetType);
            }

#endregion

        }

#endregion

#region Helper

        private bool HasResolveResourceAttribute(ParameterInfo parameter)
        {
#if (WINRT)
            return parameter.GetCustomAttributes<ResolveResourceBaseAttribute>(false).Any();
#else
            return (parameter.GetCustomAttributes(typeof(ResolveResourceBaseAttribute), false).Length > 0);
#endif
        }

#endregion

    }
}